# -*- coding: utf-8 -*
r"""
The Sage Notebook Twisted Web Server

TESTS:

It is important that this file never be imported by default on
startup by Sage, since it is very expensive, since importing Twisted
is expensive. This doctest verifies that twist.py isn't imported on
startup.::

    sage: os.system("sage -startuptime | grep twisted\.web2 1>/dev/null") != 0  # !=0 means not found
    True
"""

#############################################################################
#       Copyright (C) 2007 William Stein <wstein@gmail.com>
#  Distributed under the terms of the GNU General Public License (GPL)
#  The full text of the GPL is available at:
#                  http://www.gnu.org/licenses/
#############################################################################

############################################################
# WARNING: Potential source of confusion!!
#
# The following three global variables get set on
# startup by the script that is generated by run_notebook.py

notebook   = None
DIR        = None
OPEN_MODE  = None
SID_COOKIE = None
reactor    = None

############################################################

import os, shutil, time
import bz2
from cgi import escape

from twisted.python import log
from twisted.web2 import server, http, resource, channel
from twisted.web2 import static, http_headers, responsecode
from twisted.web2.filter import gzip
import zipfile

import css, js, keyboards, challenge

import notebook as _notebook

from sagenb.notebook.template import template

from sagenb.misc.misc import (SAGE_DOC, DATA, SAGE_VERSION, walltime,
                              tmp_filename, tmp_dir, is_package_installed,
                              jsmath_macros, encoded_str, unicode_str)

css_path             = os.path.join(DATA, "sage", "css")
image_path           = os.path.join(DATA, "sage", "images")
javascript_path      = os.path.join(DATA)
sage_javascript_path = os.path.join(DATA, 'sage', 'js')
java_path            = os.path.join(DATA)

jsmath_image_fonts = is_package_installed("jsmath-image-fonts")

# the list of users waiting to register
waiting = {}

# the user database
from user_db import UserDatabase
users = UserDatabase()

############################
# Encoding data to go between the server and client
############################
SEP = '___S_A_G_E___'

def encode_list(v):
    seq = []
    for x in v:
        x = encoded_str(x)
        seq.append(x)
    return SEP.join(seq)



############################
# Notebook autosave.
############################
# save if make a change to notebook and at least some seconds have elapsed since last save.
def init_updates():
    global save_interval, idle_interval, last_save_time, last_idle_time

    save_interval = notebook.conf()['save_interval']
    idle_interval = notebook.conf()['idle_check_interval']
    last_save_time = walltime()
    last_idle_time = walltime()

def notebook_save_check():
    global last_save_time
    t = walltime()
    if t > last_save_time + save_interval:
        notebook.save()
        last_save_time = t

def notebook_idle_check():
    global last_idle_time
    t = walltime()
    if t > last_idle_time + idle_interval:
        notebook.update_worksheet_processes()
        notebook.quit_idle_worksheet_processes()
        last_idle_time = t

def notebook_updates():
    notebook_save_check()
    notebook_idle_check()

######################################################################################
# RESOURCES
######################################################################################
def gzip_handler(request):
    """
    Add gzip compression to the request if it makes sense.
    """
    if request.host not in ('localhost', '127.0.0.1'):
        request.addResponseFilter(gzip.gzipfilter, atEnd=True)

############################
# An error message
############################
def message(msg, cont='/', username=None, **kwargs):
    template_dict = {'msg': msg, 'cont': cont, 'username': username}
    template_dict.update(kwargs)
    return template(os.path.join('html', 'error_message.html'),
                    **template_dict)

class Response(http.Response):
    """
    An adapter for ``twisted.web2.http.Response`` that automatically
    encodes its stream into UTF-8.

    INPUT:

    - code -- HTTP status code for the response

    - headers -- headers to be sent

    - stream -- content body to send
    """
    def __init__(self, code=None, headers=None, stream=None):
        if stream is not None:
            stream = encoded_str(stream)
        super(Response, self).__init__(code, headers, stream)

def HTMLResponse(*args, **kwds):
    """
    Returns an HTMLResponse object whose 'Content-Type' header has been set
    to ``text/html; charset=utf-8``

    EXAMPLES::

        sage: from sagenb.notebook.twist import HTMLResponse
        sage: response = HTMLResponse(stream='<html><head><title>Test</title></head><body>Test</body></html>')
        sage: response.headers
        <Headers: Raw: {'content-type': ['text/html; charset=utf-8']} Parsed: {'content-type': <RecalcNeeded>}>

    """
    response = Response(*args, **kwds)
    response.headers.addRawHeader('Content-Type', 'text/html; charset=utf-8')
    return response

class RedirectResponse(http.Response):
    """
    A redirect response class that uses the proper status code (303
    See Other) instead of 301 Moved Permanently, as twisted.web2
    uses. This is not cached, unlike 301.
    """
    def __init__(self, location):
        headers = http_headers.Headers()
        headers.setHeader('location', location)
        headers.setRawHeaders('cache-control', ['no-cache', 'no-store'])
        super(RedirectResponse, self).__init__(303, headers)
    
############################
# Create a Sage worksheet from a latex2html'd file
############################
doc_worksheet_number = 0
def doc_worksheet():
    global doc_worksheet_number
    wnames = notebook.worksheet_names()
    name = 'doc_browser_%s'%doc_worksheet_number
    doc_worksheet_number = doc_worksheet_number % notebook.conf()['doc_pool_size']
    if name in wnames:
        W = notebook.get_worksheet_with_name(name)
        W.clear()
    else:
        W = notebook.create_new_worksheet(name, '_sage_', docbrowser=True)
    W.set_is_doc_worksheet(True)
    return W


class WorksheetFile(resource.Resource):
    addSlash = False

    def __init__(self, path, username):
        self.docpath = path
        self.username = username

    def render(self, ctx=None):
        # Create a live Sage worksheet out of self.path and render it.
        if not os.path.exists(self.docpath):
            return HTMLResponse(stream = message('Document does not exist.'))

        doc_page_html = open(self.docpath).read()
        from docHTMLProcessor import SphinxHTMLProcessor
        doc_page = SphinxHTMLProcessor().process_doc_html(doc_page_html)

        title = extract_title(doc_page_html).replace('&mdash;','--')
        doc_page = title + '\nsystem:sage\n\n' + doc_page

        W = doc_worksheet()
        W.edit_save(doc_page)

        #FIXME: For some reason, an extra cell gets added
        #so we remove it here.
        cells = W.cell_list()
        cells.pop()

        s = notebook.html(worksheet_filename = W.filename(),
                          username = self.username)

        return HTMLResponse(stream=s)

    # This earlier code does not convert all reference manual pages into live
    # worksheets, apparently:
    #    def childFactory(self, request, name):
    #        path = self.docpath + '/' + name
    #        if name.endswith('.html'):
    #            return WorksheetFile(path, self.username)
    #        else:
    #            return static.File(path)

    # Instead, we reimplement a lower-level method to detect, convert,
    # and render live doc worksheets.  We also return appropriate
    # paths for "special" directories accessed via ../'s.  See, e.g.,
    # http://twistedmatrix.com/trac/browser/trunk/doc/web2/howto/
    # about resources and object traversal.
    def locateChild(self, request, segments):
        path = os.path.join(self.docpath, *segments)

        if segments[-1].endswith('.html'):
            return WorksheetFile(path, self.username), ()

        for special in ['_static', '_sources', '_images']:
            if special in segments:
                ind = segments.index(special)
                path = os.path.join(self.docpath, *segments[ind:])
                break

        return static.File(path), ()


############################
# The documentation browsers
############################

DOC = os.path.abspath(os.path.join(SAGE_DOC, 'output', 'html', 'en'))
DOC_PDF = os.path.abspath(os.path.join(SAGE_DOC, 'output', 'pdf'))
DOC_REF_MEDIA = os.path.abspath(os.path.join(SAGE_DOC,'en', 'reference', 'media'))

class DocPDF(resource.Resource):
    addSlash = True

    def render(self, ctx):
        return static.File(DOC_PDF)

    def childFactory(self, request, name):
        gzip_handler(request)
        return static.File(os.path.join(DOC_PDF, name))

class DocRefMedia(resource.Resource):
    addSlash = True

    def render(self, ctx):
        return static.File(DOC_REF_MEDIA)

    def childFactory(self, request, name):
        gzip_handler(request)
        return static.File(os.path.join(DOC_REF_MEDIA, name))

class DocReference(resource.Resource):
    addSlash = True
    child_media = DocRefMedia()

    def render(self, ctx):
        return static.File(os.path.join(DOC, 'reference', 'index.html'))

    def childFactory(self, request, name):
        gzip_handler(request)
        return static.File(os.path.join(DOC, 'reference', name))

class DocStatic(resource.Resource):
    addSlash = True
    child_reference = DocReference()

    def render(self, ctx):
        return static.File(os.path.join(DOC, 'index.html'))

    def childFactory(self, request, name):
        gzip_handler(request)
        return static.File(os.path.join(DOC, name))

class DocLive(resource.Resource):
    addSlash = True

    def __init__(self, username):
        self.username = username

    def render(self, ctx):
        return HTMLResponse(stream=message('nothing to see.'))

    def childFactory(self, request, name):
        gzip_handler(request)
        return WorksheetFile(os.path.join(DOC, name), username = self.username)

class Doc(resource.Resource):
    addSlash = True
    child_static = DocStatic()

    def __init__(self, username):
        self.username = username

    def render(self, ctx):
        s = notebook.html_doc(username = self.username)
        return HTMLResponse(stream=s)

    def childFactory(self, request, name):
        if name == "live":
            gzip_handler(request)
            return DocLive(username = self.username)


############################
# SageTex browser
############################
SAGETEX_PATH = ""


class SageTex(resource.Resource):
    def __init__(self, username):
        self.username = username

    def render(self, ctx):
        s = notebook.html_doc(username = self.username)
        return HTMLResponse(stream=s)

    def childFactory(self, request, name):
        return WorksheetFile(os.path.join(SAGETEX_PATH, name),
                             username = self.username)


############################
# The source code browser
############################

SRC = os.path.abspath(
    os.path.join(os.environ['SAGE_ROOT'], 'devel', 'sage', 'sage'))

class SourceBrowser(resource.Resource):
    addSlash = True

    def __init__(self, username):
        self.username = username

    def render(self, ctx):
        return static.File(SRC)

    def childFactory(self, request, name):
        return Source(os.path.join(SRC,name), self.username)

class Source(resource.Resource):
    addSlash = True

    def __init__(self, path, username):
        self.path = path
        self.username = username

    def render(self, ctx):
        filename = self.path
        if os.path.isfile(filename):
            if not os.path.exists(filename):
                src = "No such file '%s'"%filename
            else:
                src = open(filename).read()
            src = escape(src)
            return HTMLResponse(stream = template(os.path.join('html', 'source_code.html'), src_filename=self.path, src=src, username=self.username))
        else:
            return static.File(filename)

    def childFactory(self, request, name):
        return Source(os.path.join(self.path, name), self.username)


############################
# A New Worksheet
############################
class NewWorksheet(resource.Resource):
    def __init__(self, username):
        self.username = username

    def render(self, ctx):
        W = notebook.create_new_worksheet("Untitled", self.username)
        return RedirectResponse('/home/'+W.filename())


############################
# Uploading a saved worksheet file
############################

def redirect(url):
    return '<html><head><meta http-equiv="REFRESH" content="0; URL=%s"></head></html>'%url

class Upload(resource.Resource):
    def __init__(self, username):
        self.username = username

    def render(self, ctx):
        return HTMLResponse(stream = template(os.path.join('html', 'upload.html'), username=self.username))

class UploadWorksheet(resource.PostableResource):
    def __init__(self, username):
        self.username = username

    def render(self, ctx):
        backlinks = """ Return to <a href="/upload" title="Upload a worksheet"><strong>Upload File</strong></a>."""

        url = ctx.args['url'][0].strip()
        dir = ''  # we will delete the directory below if it is used
        if url != '':
            # downloading a file from the internet
            filename = tmp_filename()+".sws"
        else:
            # uploading a file from the user's computer
            dir = tmp_dir()
            filename = ctx.files['file'][0][0]
            if filename == '':
                return HTMLResponse(stream=message("Please specify a worksheet to load.%s" % backlinks))
            # Make tmp file in Sage temp directory
            filename = os.path.join(dir, filename)
            f = file(filename,'wb')
            # Then download to that file.
            f.write(ctx.files['file'][0][2].read())
            # TODO: Server blocking issues (?!)
            f.close()


        # We make a callback so that we can download a file remotely
        # while allowing the server to still serve requests.
        def callback(result):

            if ctx.args.has_key('name'):
                new_name = ctx.args['name'][0].strip()
            else:
                new_name = None

            try:
                try:

                    if filename.endswith('.zip'):
                        # Extract all the .sws files from a zip file.
                        zip_file = zipfile.ZipFile(filename)
                        sws_file = os.path.join(dir, "tmp.sws")
                        for sws in zip_file.namelist():
                            if sws.endswith('.sws'):
                                open(sws_file, 'w').write(zip_file.read(sws)) # 2.6 zip_file.extract(sws, sws_file)
                                W = notebook.import_worksheet(sws_file, self.username)
                                if new_name:
                                    W.set_name("%s - %s" % (new_name, W.name()))
                        return RedirectResponse('/')

                    else:
                        W = notebook.import_worksheet(filename, self.username)

                except Exception, msg:
                    s = 'There was an error uploading the worksheet.  It could be an old unsupported format or worse.  If you desperately need its contents contact the <a href="http://groups.google.com/group/sage-support">sage-support group</a> and post a link to your worksheet.  Alternatively, an sws file is just a bzip2 tarball; take a look inside!%s' % backlinks
                    return HTMLResponse(stream=message(s,'/'))
                finally:
                    # Clean up the temporarily uploaded filename.
                    os.unlink(filename)
                    # if a temp directory was created, we delete it now.
                    if dir:
                        shutil.rmtree(dir)

            except ValueError, msg:
                s = "Error uploading worksheet '%s'.%s" % (msg, backlinks)
                return HTMLResponse(stream = message(s, '/'))

            # If the user requested in the form a specific title for
            # the worksheet set it.
            if new_name:
                W.set_name(new_name)

            return RedirectResponse('/home/'+W.filename())

        if url != '':
            # We use the downloadPage function which returns a
            # deferred which we are allowed to return to the server.
            # The server waits until the download is finished and then runs
            # the callback function specified.
            from twisted.web.client import downloadPage
            d = downloadPage(url, filename)
            d.addCallback(callback)
            def errback(result):
                msg = "There was an error uploading '%s' (please recheck the URL).%s" % (url, backlinks)
                return HTMLResponse(stream=message(msg,'/'))
            d.addErrback(errback)
            return d
        else:
            # If we already have the file, then we
            # can just return the result of callback which will
            # give us the HTMLResponse.
            return callback(None)


############################
# A resource attached to a given worksheet.
#
# This has the name of the worksheet and the
# worksheet object itself set as attributes.
# It's much better to do it once-for-all here
# instead of doing it in the derived classes
# over and over again.
############################
class WorksheetResource:
    def __init__(self, name, username):
        self.name = name
        self.username = username
        self.worksheet = notebook.get_worksheet_with_filename(name)
        if not self.worksheet.is_published():
            self.worksheet.set_active(username)
        owner = self.worksheet.owner()
        if owner != '_sage_' and username != owner:
            if not self.worksheet.is_published():
                if not username in self.worksheet.collaborators() and user_type(username) != 'admin':
                    raise RuntimeError, "illegal worksheet access"

    def id(self, ctx):
        # We cast the incoming cell ID to an integer, if it's
        # possible.  Otherwise, we treat it as a string.
        try:
            return int(ctx.args['id'][0])
        except ValueError:
            return ctx.args['id'][0]


###############################################
# Worksheet data -- a file that
# is associated with a cell in some worksheet.
# The file is stored on the filesystem.
#      /home/worksheet_name/data/cell_number/filename
##############################################
class Worksheet_savedatafile(WorksheetResource, resource.PostableResource):
    def render(self, ctx):
        if ctx.args.has_key('button_save'):
            E = ctx.args['textfield'][0]
            filename = ctx.args['filename'][0]
            dest = os.path.join(self.worksheet.data_directory(), filename)
            if os.path.exists(dest): os.unlink(dest)
            open(dest,'w').write(E)
        return RedirectResponse('/home/'+self.worksheet.filename())

class Worksheet_link_datafile(WorksheetResource, resource.Resource):
    def render(self, ctx):
        target_worksheet_filename = ctx.args['target'][0]
        data_filename = ctx.args['filename'][0]
        src = os.path.abspath(os.path.join(
            self.worksheet.data_directory(), data_filename))
        target_ws =  notebook.get_worksheet_with_filename(target_worksheet_filename)
        target = os.path.abspath(os.path.join(
            target_ws.data_directory(), data_filename))
        if target_ws.owner() != self.username and not target_ws.is_collaborator(self.username):
            return HTMLResponse(stream=message("illegal link attempt!"))
        os.system('ln "%s" "%s"'%(src, target))
        return RedirectResponse('/home/'+target_ws.filename() + '/datafile?name=%s'%data_filename)


class Worksheet_upload_data(WorksheetResource, resource.Resource):
    def render(self, ctx):
        return HTMLResponse(stream = notebook.html_upload_data_window(self.worksheet, self.username))

class Worksheet_do_upload_data(WorksheetResource, resource.PostableResource):
    def render(self, ctx):
        # Backlinks.
        worksheet_url = '/home/' + self.worksheet.filename()
        upload_url = worksheet_url + '/upload_data'
        backlinks = """ Return to <a href="%s" title="Upload or create a data file in a wide range of formats"><strong>Upload or Create Data File</strong></a> or <a href="%s" title="Interactively use the worksheet"><strong>%s</strong></a>.""" % (upload_url, worksheet_url, self.worksheet.name())

        # Check for the form's fields.  They need not be filled.
        if not ctx.files.has_key('file'):
            return HTMLResponse(stream=message('Error uploading file (missing fileField file).%s' % backlinks, worksheet_url))

        text_fields = ['url', 'new', 'name']
        for field in text_fields:
            if not ctx.args.has_key(field):
                return HTMLResponse(stream=message('Error uploading file (missing %s arg).%s' % (field, backlinks), worksheet_url))

        # Get the fields.
        newfield = ctx.args.get('new', [''])[0].strip()
        name = ctx.args.get('name', [''])[0].strip()
        url = ctx.args.get('url', [''])[0].strip()

        name = name or ctx.files['file'][0][0] or newfield
        if url and not name:
            name = url.split('/')[-1]

        # The next line makes sure that name is plain filename, so
        # that dest below is a file in the data directory.  See trac
        # 7495 for why this is critically important.
        name = os.path.split(name)[-1]

        if not name:
            return HTMLResponse(stream=message('Error uploading file (missing filename).%s' % backlinks, worksheet_url))
        dest = os.path.join(self.worksheet.data_directory(), name)
        if os.path.exists(dest):
            if not os.path.isfile(dest):
                return HTMLResponse(stream=message('Suspicious filename "%s" encountered uploading file.%s' % (name, backlinks), worksheet_url))
            os.unlink(dest)

        response = RedirectResponse(worksheet_url + '/datafile?name=%s' % name)

        if url != '':
            # Here we use twisted's downloadPage function which
            # returns a deferred object.  We return the deferred to
            # the server, and it will wait until the download has
            # finished while still serving other requests.  At the end
            # of the deferred callback chain should be the response
            # that we wanted to return.
            from twisted.web.client import downloadPage

            # The callback just returns the response
            def callback(result):
                return response
            def errback(result):
                msg = "There was an error uploading '%s' (please recheck the URL).%s" % (url, backlinks)
                return HTMLResponse(stream=message(msg, worksheet_url))

            d = downloadPage(url, dest)
            d.addCallback(callback)
            d.addErrback(errback)
            return d
        elif newfield:
            open(dest, 'w').close()
            return response
        else:
            f = file(dest, 'wb')
            f.write(ctx.files['file'][0][2].read())
            f.close()
            return response


##############################################
# Download or delete a data file
##############################################

class Worksheet_datafile(WorksheetResource, resource.Resource):
    def render(self, ctx):
        dir = os.path.abspath(self.worksheet.data_directory())
        filename = ctx.args['name'][0]
        if ctx.args.has_key('action'):
            if ctx.args['action'][0] == 'delete':
                path = os.path.join(self.worksheet.data_directory(), filename)
                os.unlink(path)
                return HTMLResponse(stream = message("Successfully deleted '%s'"%filename,
                                                      '/home/' + self.worksheet.filename(),
                                                     title=u'Data file deleted'))
        s = notebook.html_download_or_delete_datafile(self.worksheet, self.username, filename)
        return HTMLResponse(stream=s)

##############################################
# Returns an object in the datafile directory
##############################################
class Worksheet_data(WorksheetResource, resource.Resource):
    addSlash = True

    def render(self, ctx):
        dir = os.path.abspath(self.worksheet.data_directory())
        if os.path.exists(dir):
            return static.File(dir)
        else:
            return HTMLResponse(stream = message("No data files",'..'))

    def childFactory(self, request, name):
        dir = os.path.abspath(self.worksheet.data_directory())
        return static.File(os.path.join(dir, name))


class CellData(resource.Resource):
    def __init__(self, worksheet, number):
        self.worksheet = worksheet
        self.number = number

    def render(self, ctx):
        return HTMLResponse(stream = message("No data file (%s)"%self.number,'..'))

    def childFactory(self, request, name):
        dir = self.worksheet.directory()
        path = os.path.join(dir, 'cells', str(self.number), name)
        request.setLastModified(os.stat(filename).st_mtime)
        return static.File(path)


class Worksheet_cells(WorksheetResource, resource.Resource):
    addSlash = True

    def render(self, ctx):
        return static.File(self.worksheet.cells_directory())

    def childFactory(self, request, segment):
        return static.File(os.path.join(self.worksheet.cells_directory(), segment))

########################################################
# keep alive
########################################################

class Worksheet_alive(WorksheetResource, resource.Resource):
    def render(self, ctx):
        return HTMLResponse(stream = str(self.worksheet.state_number()))

########################################################
# Worksheet configuration.
########################################################
class Worksheet_conf(WorksheetResource, resource.Resource):
    def render(self, ctx):
        conf = self.worksheet.conf()
        s = str(conf)
        # TODO: This should be a form that allows for configuring all options
        # of a given worksheet, saves the result,
        return HTMLResponse(stream = s)

class TrivialResource(resource.Resource):
    def render(self, ctx):
        return HTMLResponse(stream="success")

class Worksheet_system(WorksheetResource, resource.Resource):
    def childFactory(self, request, system):
        self.worksheet.set_system(system)
        return TrivialResource()

class Worksheet_pretty_print(WorksheetResource, resource.Resource):
    def childFactory(self, request, enable):
        self.worksheet.set_pretty_print(enable)
        return TrivialResource()



########################################################
# Cell introspection
########################################################
class Worksheet_introspect(WorksheetResource, resource.PostableResource):
    """
    Cell introspection. This is called when the user presses the tab
    key in the browser in order to introspect.
    """
    def render(self, ctx):
        try:
            id = self.id(ctx)
        except (KeyError,TypeError):
            return HTMLResponse(stream = 'Error in introspection -- invalid cell id.')
        try:
            before_cursor = ctx.args['before_cursor'][0]
        except KeyError:
            before_cursor = ''
        try:
            after_cursor = ctx.args['after_cursor'][0]
        except KeyError:
            after_cursor = ''
        C = self.worksheet.get_cell_with_id(id)
        C.evaluate(introspect=[before_cursor, after_cursor])
        return HTMLResponse(stream = encode_list([C.next_id(),'introspect',id]))

########################################################
# Edit the entire worksheet
########################################################
class Worksheet_edit(WorksheetResource, resource.Resource):
    """
    Return a window that allows the user to edit the text of the
    worksheet with the given filename.
    """
    def render(self, ctx):
        s = notebook.html_edit_window(self.worksheet, self.username)
        return HTMLResponse(stream = s)

########################################################
# Plain text log view of worksheet
########################################################
class Worksheet_text(WorksheetResource, resource.Resource):
    """
    Return a window that allows the user to edit the text of the
    worksheet with the given filename.
    """
    def render(self, ctx):
        s = notebook.html_plain_text_window(self.worksheet, self.username)
        return HTMLResponse(stream = s)

########################################################
# Copy a worksheet
########################################################
class Worksheet_copy(WorksheetResource, resource.PostableResource):
    def render(self, ctx):
        W = notebook.copy_worksheet(self.worksheet, self.username)
        if 'no_load' in ctx.args:
            return http.StatusResponse(200, '')
        else:
            return RedirectResponse('/home/' + W.filename())

########################################################
# Get a copy of a published worksheet and start editing it
########################################################
class Worksheet_edit_published_page(WorksheetResource, resource.Resource):
    def render(self, ctx):
        if user_type(self.username) == 'guest':
            return HTMLResponse(stream = message(
                'You must <a href="/">login first</a> in order to edit this worksheet.'))
        ws = self.worksheet.worksheet_that_was_published()
        if ws.owner() == self.username:
            W = ws
        else:
            W = notebook.copy_worksheet(self.worksheet, self.username)
            W.set_name(self.worksheet.name())
        return RedirectResponse('/home/' + W.filename())

########################################################
# Save a worksheet
########################################################
class Worksheet_save(WorksheetResource, resource.PostableResource):
    """
    Save the contents of a worksheet after editing it in plain-text
    edit mode.
    """
    def render(self, ctx):
        if ctx.args.has_key('button_save'):
            E = ctx.args['textfield'][0]
            self.worksheet.edit_save(E)
            self.worksheet.record_edit(self.username)
        return RedirectResponse('/home/'+self.worksheet.filename())


class Worksheet_save_snapshot(WorksheetResource, resource.PostableResource):
    """
    Save a snapshot of a worksheet.
    """
    def render(self, ctx):
        self.worksheet.save_snapshot(self.username)
        return HTMLResponse(stream="saved")

class Worksheet_save_and_quit(WorksheetResource, resource.PostableResource):
    """
    Save a snapshot of a worksheet and quit.
    """
    def render(self, ctx):
        self.worksheet.save_snapshot(self.username)
        self.worksheet.quit()
        return HTMLResponse(stream="saved")

class Worksheet_discard_and_quit(WorksheetResource, resource.PostableResource):
    """
    Save a snapshot of a worksheet and quit.
    """
    def render(self, ctx):
        self.worksheet.revert_to_last_saved_state()
        self.worksheet.quit()
        return HTMLResponse(stream="saved")

class Worksheet_revert_to_last_saved_state(WorksheetResource, resource.PostableResource):
    def render(self, ctx):
        self.worksheet.revert_to_last_saved_state()
        return HTMLResponse(stream="reverted")

class Worksheet_save_and_close(WorksheetResource, resource.PostableResource):
    """
    Save a snapshot of a worksheet then quit it.
    """
    def render(self, ctx):
        self.worksheet.save_snapshot(self.username)
        self.worksheet.quit()
        return HTMLResponse(stream="saved")

########################################################
# Collaborate with others
########################################################
class Worksheet_share(WorksheetResource, resource.Resource):
    def render(self, ctx):
        s = notebook.html_share(self.worksheet, self.username)
        return HTMLResponse(stream = s)

class Worksheet_invite_collab(WorksheetResource, resource.PostableResource):
    def render(self, ctx):
        if not ctx.args.has_key('collaborators'):
            v = []
        else:
            collab = ctx.args['collaborators'][0]
            v = [x.strip() for x in collab.split(',')]
        self.worksheet.set_collaborators(v)
        return RedirectResponse('.')


#################################
# Revisions
#################################

class PublishWorksheetRevision(resource.Resource):
    def __init__(self, worksheet, rev):
        self.worksheet = worksheet
        self.rev = rev

    def render(self, ctx):
        W = notebook.publish_worksheet(self.worksheet, self.username)
        txt = open(self.worksheet.get_snapshot_text_filename(self.rev)).read()
        W.delete_cells_directory()
        W.edit_save(txt)
        return RedirectResponse('/home/'+W.filename())

class RevertToWorksheetRevision(resource.Resource):
    def __init__(self, worksheet, rev):
        self.worksheet = worksheet
        self.rev = rev

    def render(self, ctx):
        self.worksheet.save_snapshot(self.username)
        txt = open(self.worksheet.get_snapshot_text_filename(self.rev)).read()
        self.worksheet.delete_cells_directory()
        self.worksheet.edit_save(txt)
        return RedirectResponse('/home/'+self.worksheet.filename())

def worksheet_revision_publish(worksheet, rev, username):
    W = notebook.publish_worksheet(worksheet, username)
    txt = bz2.decompress(open(worksheet.get_snapshot_text_filename(rev)).read())
    W.delete_cells_directory()
    W.edit_save(txt)
    return RedirectResponse('/home/'+W.filename())

def worksheet_revision_revert(worksheet, rev, username):
    worksheet.save_snapshot(username)
    txt = bz2.decompress(open(worksheet.get_snapshot_text_filename(rev)).read())
    worksheet.delete_cells_directory()
    worksheet.edit_save(txt)
    return RedirectResponse('/home/'+worksheet.filename())


class Worksheet_revisions(WorksheetResource, resource.PostableResource):
    """
    Show a list of revisions of this worksheet.
    """
    def render(self, ctx):
        if not ctx.args.has_key('action'):
            if ctx.args.has_key('rev'):
                rev = ctx.args['rev'][0]
                s = notebook.html_specific_revision(self.username, self.worksheet, rev)
            else:
                s = notebook.html_worksheet_revision_list(self.username, self.worksheet)
        else:
            rev = ctx.args['rev'][0]
            action = ctx.args['action'][0]
            if action == 'revert':
                return worksheet_revision_revert(self.worksheet, rev, self.username)
            elif action == 'publish':
                return worksheet_revision_publish(self.worksheet, rev, self.username)
            else:
                s = message('Error')
        return HTMLResponse(stream = s)


########################################################
# User Settings and Notebook Configuration
########################################################

class SettingsPage(resource.PostableResource):
    def __init__(self, username):
        self.username = username
        self.nb_user = notebook.user(username)

    def render(self, request):
        error = None
        redirect_to_home = None
        redirect_to_logout  = None
        nu = self.nb_user

        autosave = int(request.args.get('autosave', [0])[0]) * 60
        if autosave:
            nu['autosave_interval'] = autosave
            redirect_to_home = True

        old = request.args.get('old-pass', [None])[0]
        new = request.args.get('new-pass', [None])[0]
        two = request.args.get('retype-pass', [None])[0]
        if new or two:
            if not old:
                error = 'Old password not given'
            elif not nu.password_is(old):
                error = 'Incorrect password given'
            elif not new:
                error = 'New password not given'
            elif not two:
                error = 'Please type in new password again.'
            elif new != two:
                error = 'The passwords you entered do not match.'

            if not error:
                # The browser may auto-fill in "old password," even
                # though the user may not want to change her password.
                notebook.change_password(self.username, new)
                redirect_to_logout = True

        if notebook.conf()['email']:
            newemail = request.args.get('new-email', [None])[0]
            if newemail:
                nu.set_email(newemail)
#                nu.set_email_confirmation(False)
                redirect_to_home = True

        if error:
            return HTMLResponse(stream = message(error, '/settings'))

        if redirect_to_logout:
            return RedirectResponse('/logout')

        if redirect_to_home:
            return RedirectResponse('/home/%s' % self.username)

        td = {}
        td['username'] = self.username

        td['autosave_intervals'] = ((i, ' selected') if nu['autosave_interval']/60 == i else (i, '') for i in range(1, 10, 2))

        td['email'] = notebook.conf()['email']
        if td['email']:
            td['email_address'] = nu.get_email() or 'None'
            if nu.is_email_confirmed():
                td['email_confirmed'] = 'Confirmed'
            else:
                td['email_confirmed'] = 'Not confirmed'

        td['admin'] = nu.is_admin()

        s = template(os.path.join('html', 'settings', 'account_settings.html'), **td)
        return HTMLResponse(stream = s)


class NotebookSettingsPage(resource.PostableResource):
    def __init__(self, username):
        self.username = username

    def render(self, request):
        updated = {}
        if 'form' in request.args:
            updated = notebook.conf().update_from_form(request.args)

        template_dict = {}
        template_dict['auto_table'] = notebook.conf().html_table(updated)
        template_dict['admin'] = notebook.user(self.username).is_admin()
        template_dict['username'] = self.username
        s = template(os.path.join('html', 'settings', 'notebook_settings.html'),
                     **template_dict)
        return HTMLResponse(stream = s)


########################################################
# Used in refreshing the cell list
########################################################
class Worksheet_cell_list(WorksheetResource, resource.PostableResource):
    """
    Return the state number and the HTML for the main body of the
    worksheet, which consists of a list of cells.
    """
    def render(self, ctx):
        W = self.worksheet
        # TODO: Send and actually use the body's HTML.
        v = encode_list([W.state_number(), ''])
#        v = encode_list([W.state_number(), W.html_cell_list()])
        return HTMLResponse(stream=v)


########################################################
# Set output type of a cell
########################################################
class Worksheet_set_cell_output_type(WorksheetResource, resource.PostableResource):
    """
    Set the output type of the cell.

    This enables the type of output cell, such as to allowing wrapping
    for output that is very long.
    """
    def render(self, ctx):
        id = self.id(ctx)
        typ = ctx.args['type'][0]
        W = self.worksheet
        W.get_cell_with_id(id).set_cell_output_type(typ)
        return HTMLResponse(stream = '')

########################################################
# The new cell command: /home/worksheet/new_cell?id=number
########################################################
class Worksheet_new_cell_before(WorksheetResource, resource.PostableResource):
    """
    Adds a new cell before a given cell.
    """
    def render(self, ctx):
        id = self.id(ctx)
        if not ctx.args.has_key('input'):
            input = u''
        else:
            input = ctx.args['input'][0]
            input = unicode_str(input)

        cell = self.worksheet.new_cell_before(id, input=input)
        self.worksheet.increase_state_number()
        s = encode_list([cell.id(), cell.html(div_wrap=False), id])
        return HTMLResponse(stream = s)

class Worksheet_new_text_cell_before(WorksheetResource, resource.PostableResource):
    """
    Adds a new cell before a given cell.
    """
    def render(self, ctx):
        id = self.id(ctx)
        if not ctx.args.has_key('input'):
            input = ''
        else:
            input = ctx.args['input'][0]
            input = unicode_str(input)

        cell = self.worksheet.new_text_cell_before(id, input=input)
        s = encode_list([cell.id(), cell.html(editing=True), id])
        return HTMLResponse(stream = s)


class Worksheet_new_cell_after(WorksheetResource, resource.PostableResource):
    """
    Adds a new cell after a given cell.
    """
    def render(self, ctx):
        id = self.id(ctx)
        if not ctx.args.has_key('input'):
            input = ''
        else:
            input = ctx.args['input'][0]
            input = unicode_str(input)

        cell = self.worksheet.new_cell_after(id, input=input)
        s = encode_list([cell.id(), cell.html(div_wrap=False), id])
        return HTMLResponse(stream = s)

class Worksheet_new_text_cell_after(WorksheetResource, resource.PostableResource):
    """
    Adds a new text cell after a given cell.
    """
    def render(self, ctx):
        id = self.id(ctx)
        if not ctx.args.has_key('input'):
            input = ''
        else:
            input = ctx.args['input'][0]
            input = unicode_str(input)

        cell = self.worksheet.new_text_cell_after(id, input=input)
        s = encode_list([cell.id(), cell.html(editing=True), id])
        return HTMLResponse(stream = s)


########################################################
# The delete cell command: /home/worksheet/delete_cell?id=number
########################################################
class Worksheet_delete_cell(WorksheetResource, resource.PostableResource):
    """
    Deletes a notebook cell.

    If there is only one cell left in a given worksheet, the request to
    delete that cell is ignored because there must be a least one cell
    at all times in a worksheet. (This requirement exists so other
    functions that evaluate relative to existing cells will still work,
    and so one can add new cells.)
    """
    def render(self, ctx):
        id = self.id(ctx)
        W = self.worksheet
        if len(W.compute_cell_id_list()) <= 1:
            s = 'ignore'
        else:
            prev_id = W.delete_cell_with_id(id)
            s = encode_list(['delete', id, prev_id, W.cell_id_list()])
        return HTMLResponse(stream = s)

##############################################################################
# The delete cell output command: /home/worksheet/delete_cell_output?id=number
##############################################################################
class Worksheet_delete_cell_output(WorksheetResource, resource.PostableResource):
    """
    Deletes a cell's output.
    """
    def render(self, ctx):
        id = self.id(ctx)
        self.worksheet.get_cell_with_id(id).delete_output()
        return HTMLResponse(stream = encode_list(['delete_output', id]))


############################
# Get the latest update on output appearing
# in a given output cell.
############################
class Worksheet_cell_update(WorksheetResource, resource.PostableResource):
    def render(self, ctx):
        id = self.id(ctx)

        worksheet = self.worksheet

        # update the computation one "step".
        worksheet.check_comp()

        # now get latest status on our cell
        status, cell = worksheet.check_cell(id)

        if status == 'd':
            new_input = cell.changed_input_text()
            out_html = cell.output_html()
            H = "Worksheet '%s' (%s)\n"%(worksheet.name(), time.strftime("%Y-%m-%d at %H:%M",time.localtime(time.time())))
            H += cell.edit_text(ncols=notebook.HISTORY_NCOLS, prompts=False,
                                max_out=notebook.HISTORY_MAX_OUTPUT)
            notebook.add_to_user_history(H, self.username)
        else:
            new_input = ''
            out_html = ''

        if cell.interrupted():
            inter = 'true'
        else:
            inter = 'false'

        raw = cell.output_text(raw=True).split("\n")
        if "Unhandled SIGSEGV" in raw:
            inter = 'restart'
            print "Segmentation fault detected in output!"
        msg = '%s%s %s'%(status, cell.id(),
                       encode_list([cell.output_text(html=True),
                                    cell.output_text(notebook.conf()['word_wrap_cols'], html=True),
                                    out_html,
                                    new_input,
                                    inter,
                                    cell.introspect_html()]))

        # There may be more computations left to do, so start one if there is one.
        worksheet.start_next_comp()

        return HTMLResponse(stream=msg)


class Worksheet_eval(WorksheetResource, resource.PostableResource):
    """
    Evaluate a worksheet cell.

    If the request is not authorized (the requester did not enter the
    correct password for the given worksheet), then the request to
    evaluate or introspect the cell is ignored.

    If the cell contains either 1 or 2 question marks at the end (not
    on a comment line), then this is interpreted as a request for
    either introspection to the documentation of the function, or the
    documentation of the function and the source code of the function
    respectively.
    """
    def render(self, ctx):
        id = self.id(ctx)
        if not ctx.args.has_key('input'):
            input_text = u''
        else:
            input_text = ctx.args['input'][0]
            input_text = input_text.replace('\r\n', '\n')   # DOS
            input_text = unicode_str(input_text)

        W = self.worksheet
        W.increase_state_number()
        owner = W.owner()
        if owner != '_sage_':
            if W.owner() != self.username and not (self.username in W.collaborators()):
               return InvalidPage(msg = "can't evaluate worksheet cells", username = self.username)
        cell = W.get_cell_with_id(id)

        cell.set_input_text(input_text)

        if ctx.args.has_key('save_only') and ctx.args['save_only'][0] == '1':
            notebook_updates()
            return HTMLResponse(stream='')
        elif ctx.args.has_key('text_only') and ctx.args['text_only'][0] == '1':
            notebook_updates()
            return HTMLResponse(stream=encode_list([str(id), cell.html()]))
        else:
            newcell = int(ctx.args['newcell'][0])  # whether to insert a new cell or not

        cell.evaluate(username = self.username)

        if cell.is_last():
            new_cell = W.append_new_cell()
            s = encode_list([new_cell.id(), 'append_new_cell', new_cell.html(div_wrap=False)])
        elif newcell:
            new_cell = W.new_cell_after(id)
            s = encode_list([new_cell.id(), 'insert_cell', new_cell.html(div_wrap=False), str(id)])
        else:
            s = encode_list([cell.next_id(), 'no_new_cell', str(id)])

        notebook_updates()
        return HTMLResponse(stream=s)


########################################################
# Publication and rating of a worksheet
########################################################

class Worksheet_publish(WorksheetResource, resource.Resource):
    """
    This is a child resource of the Worksheet resource. It provides a
    frontend to the management of worksheet publication. This management
    functionality includes initializational of publication,
    re-publication, automated publication when a worksheet saved, and
    ending of publication.
    """
    addSlash = True

    def render(self, ctx):
        # Publishes worksheet and also sets worksheet to be published automatically when saved
        if 'yes' in ctx.args and 'auto' in ctx.args:
            notebook.publish_worksheet(self.worksheet, self.username)
            self.worksheet.set_auto_publish(True)
            return RedirectResponse("/home/%s/publish" % (self.worksheet.filename()))
        # Just publishes worksheet
        elif 'yes' in ctx.args:
            notebook.publish_worksheet(self.worksheet, self.username)
            return RedirectResponse("/home/%s/publish" % (self.worksheet.filename()))
        # Stops publication of worksheet
        elif 'stop' in ctx.args:
            notebook.delete_worksheet(self.worksheet.published_version().filename())
            return RedirectResponse("/home/%s/publish" % (self.worksheet.filename()))
        # Re-publishes worksheet
        elif 're' in ctx.args:
            W = notebook.publish_worksheet(self.worksheet, self.username)
            return RedirectResponse("/home/%s/publish" % (self.worksheet.filename()))
        # Sets worksheet to be published automatically when saved
        elif 'auto' in ctx.args:
            self.worksheet.set_auto_publish(not self.worksheet.is_auto_publish())
            return RedirectResponse("/home/%s/publish" % (self.worksheet.filename()))
        # Returns boolean of "Is this worksheet set to be published automatically when saved?"
        elif 'is_auto' in ctx.args:
            return HTMLResponse(stream=str(self.worksheet.is_auto_publish()))
        # Returns the publication page
        else:
            # Page for when worksheet already published
            if self.worksheet.has_published_version():
                if ctx.headers.hasHeader('host'):
                    hostname = ctx.headers.getHeader('host')
                else:
                    hostname = notebook.interface + ':' + notebook.port
                addr = 'http%s://%s/home/%s' % ('' if not notebook.secure else 's',
                                                hostname,
                                                self.worksheet.published_version().filename())
                dtime = self.worksheet.published_version().date_edited()
                return HTMLResponse(stream=notebook.html_afterpublish_window(self.worksheet, self.username, addr, dtime))
            # Page for when worksheet is not already published
            else:
                return HTMLResponse(stream=notebook.html_beforepublish_window(self.worksheet, self.username))


class Worksheet_rating_info(WorksheetResource, resource.Resource):
    def render(self, ctx):
        s = self.worksheet.html_ratings_info()
        return HTMLResponse(stream=s)

class Worksheet_rate(WorksheetResource, resource.Resource):
    def render(self, ctx):
        ret = '/home/' + self.worksheet.filename()
        #if self.worksheet.is_rater(self.username):
        #    return HTMLResponse(stream=message("You have already rated the worksheet <i><b>%s</b></i>."%self.worksheet.name(), ret))
        if user_type(self.username) == "guest":
            return HTMLResponse(stream = message(
                'You must <a href="/">login first</a> in order to rate this worksheet.', ret))

        rating = int(ctx.args['rating'][0])
        if rating < 0 or rating >= 5:
            return HTMLResponse(stream = message(
                "Gees -- You can't fool the rating system that easily!", ret))
        comment = ctx.args['comment'][0]
        self.worksheet.rate(rating, comment, self.username)
        return HTMLResponse(stream=message("""
        Thank you for rating the worksheet <b><i>%s</i></b>!
        You can <a href="rating_info">see all ratings of this worksheet.</a>
        """%self.worksheet.name(), '/pub/', title=u'Rating Accepted'))


########################################################
# Downloading, moving around, renaming, etc.
########################################################


class Worksheet_download(WorksheetResource, resource.Resource):
    def childFactory(self, request, tmp_title):
        worksheet_name = self.name
        filename = tmp_filename() + '.sws'
        if tmp_title.endswith('.sws'):
            tmp_title = tmp_title[:-4]
        try:
            notebook.export_worksheet(worksheet_name, filename, tmp_title)
        except KeyError:
            return HTMLResponse(stream=message('No such worksheet.'))
        r = open(filename, 'rb').read()
        os.unlink(filename)
        return static.Data(r, 'application/sage')

class DownloadWorksheets(resource.Resource):

    def __init__(self, username):
        self.username = username

    def render(self, ctx):
        def f():
            from sagenb.misc.misc import walltime
            t = walltime()
            print "Starting zipping a group of worksheets in a separate thread..."
            zip_filename = tmp_filename() + ".zip"
            # child
            worksheet_names = set()
            if ctx.args.has_key('filenames'):
                sep = ctx.args['sep'][0]
                worksheets = [notebook.get_worksheet_with_filename(x.strip())
                              for x in ctx.args['filenames'][0].split(sep)
                              if len(x.strip()) > 0]
            else:
                worksheets = notebook.worksheet_list_for_user(self.username)

            zip = zipfile.ZipFile(zip_filename, 'w', zipfile.ZIP_STORED)
            for worksheet in worksheets:
                sws_filename = tmp_filename() + '.sws'
                notebook.export_worksheet(worksheet.filename(), sws_filename)
                entry_name = worksheet.name()
                if entry_name in worksheet_names:
                    i = 2
                    while ("%s_%s" % (entry_name, i)) in worksheet_names:
                        i += 1
                    entry_name = "%s_%s" % (entry_name, i)
                zip.write(sws_filename, entry_name + ".sws")
                os.unlink(sws_filename)
            zip.close()
            r = open(zip_filename, 'rb').read()
            os.unlink(zip_filename)
            print "Finished zipping %s worksheets (%s seconds)"%(len(worksheets), walltime(t))
            return static.Data(r, 'application/zip')

        from twisted.internet import threads
        return threads.deferToThread(f)

class Worksheet_rename(WorksheetResource, resource.PostableResource):
    def render(self, ctx):
        self.worksheet.set_name(ctx.args['name'][0])
        return HTMLResponse(stream='done')

class Worksheet_restart_sage(WorksheetResource, resource.Resource):
    def render(self, ctx):
        # TODO -- this must not block long (!)
        self.worksheet.restart_sage()
        return HTMLResponse(stream='done')

class Worksheet_quit_sage(WorksheetResource, resource.Resource):
    def render(self, ctx):
        # TODO -- this must not block long (!)
        self.worksheet.quit()
        return HTMLResponse(stream='done')

class Worksheet_interrupt(WorksheetResource, resource.Resource):
    def render(self, ctx):
        # TODO -- this must not block long (!)
        def callback(success):
            """Called when interrupt is successful"""
            return HTMLResponse(stream = 'success' if success else 'failed')
        
        deferred = self.worksheet.interrupt(callback)
        return deferred

class Worksheet_hide_all(WorksheetResource, resource.Resource):
    def render(self, ctx):
        self.worksheet.hide_all()
        return HTMLResponse(stream='success')

class Worksheet_show_all(WorksheetResource, resource.Resource):
    def render(self, ctx):
        self.worksheet.show_all()
        return HTMLResponse(stream='success')


# Delete all the output of cells in a worksheet.
class Worksheet_delete_all_output(WorksheetResource, resource.Resource):
    def render(self, ctx):
        try:
            self.worksheet.delete_all_output(self.username)
        except ValueError:
            return HTMLResponse(stream='fail')
        return HTMLResponse(stream='success')

class Worksheet_print(WorksheetResource, resource.Resource):
    def render(self, ctx):
        s = notebook.html(self.name, do_print=True)
        return HTMLResponse(stream=s)


class NotImplementedWorksheetOp(resource.Resource):
    def __init__(self, op, ws):
        self.op = op
        self.ws = ws

    def render(self, ctx):
        return HTMLResponse(stream = message(
            'The worksheet operation "%s" is not defined.'%self.op,
            '/home/'+self.ws.filename()))


class Worksheet(WorksheetResource, resource.Resource):
    addSlash = True

    def render(self, ctx):
        self.worksheet.sage()
        s = notebook.html(worksheet_filename = self.name,  username=self.username)
        return HTMLResponse(stream=s)

    def childFactory(self, request, op):
        notebook_updates()
        try:
            # Rather than a bunch of if-else statements, we wrap
            # any Worksheet_... class as a subresource of a worksheet
            # using the following  statement:
            if (self.name.startswith('pub/') and
                op not in ['alive', 'cells', 'data', 'download',
                           'edit_published_page', 'rate', 'rating_info']):
                raise KeyError

            R = globals()['Worksheet_%s'%op]
            return R(self.name, username = self.username)
        except KeyError:
            file = os.path.join(self.worksheet.data_directory(), op)
            if os.path.exists(file):
                return static.File(file)
            dir = self.worksheet.cells_directory()
            for F in os.listdir(dir):
                h = os.path.join(dir,F,op)
                if os.path.exists(h):
                    return static.File(h)
            return NotImplementedWorksheetOp(op, self.worksheet)


def render_worksheet_list(args, pub, username):
    """
    Returns a rendered worksheet listing.

    INPUT:

    -  ``args`` - ctx.args where ctx is the dict passed
       into a resource's render method

    -  ``pub`` - boolean, True if this is a listing of
       public worksheets

    -  ``username`` - the user whose worksheets we are
       listing

    OUTPUT:

    a string
    """
    from sagenb.notebook.notebook import sort_worksheet_list
    typ = args['typ'][0] if 'typ' in args else 'active'
    search = unicode_str(args['search'][0]) if 'search' in args else None
    sort = args['sort'][0] if 'sort' in args else 'last_edited'
    reverse = (args['reverse'][0] == 'True') if 'reverse' in args else False

    if not pub:
        worksheets = notebook.worksheet_list_for_user(username, typ=typ, sort=sort,
                                                      search=search, reverse=reverse)

    else:
        worksheets = notebook.worksheet_list_for_public(username, sort=sort,
                                                        search=search, reverse=reverse)

    worksheet_filenames = [x.filename() for x in worksheets]

    if pub and (not username or username == tuple([])):
        username = 'pub'

    accounts = notebook.get_accounts()

    return template(os.path.join('html', 'worksheet_listing.html'), **locals())


class WorksheetsByUser(resource.Resource):
    addSlash = True

    def __init__(self, user, username):
        # user -- who we're requesting the worksheets of
        # username -- who is doing the requesting
        self.user = user
        self.username = username

    def render_list(self, ctx):
        s = render_worksheet_list(ctx.args, pub=False, username=self.user)
        return HTMLResponse(stream = s)

    def render(self, ctx):
        if self.user == self.username or user_type(self.username) == 'admin':
            return self.render_list(ctx)
        else:
            if not self.username == 'guest':
                s = message("User '%s' does not have permission to view the home page of '%s'."%(self.username, self.user))
            else:
                return RedirectResponse('/')
            return HTMLResponse(stream = s)

    def childFactory(self, request, name):
        filename = self.user + '/' + name
        try:
            return Worksheet(filename, self.username)
        except KeyError:
            if self.user != 'pub':
                s = "The user '%s' has no worksheet '%s'."%(self.user, name)
                return InvalidPage(msg = s, username = self.user)
            else:
                s = 'There is no published worksheet with name "%s".  Redirecting to the index of published worksheets in 10 seconds...<br><br>' % name
                return InvalidPage(msg = s, username = self.username,
                                   cont = '/pub', redirect_url = '/pub',
                                   redirect_delay = 10)
        except RuntimeError:
            s = "You are not logged in or do not have access to the worksheet '%s'."%name
            return InvalidPage(msg = s, username = self.user)



############################
# Trash can, archive and active
############################
class EmptyTrash(resource.PostableResource):
    def __init__(self, username):
        """
        This twisted resource empties the trash of the current user when it
        is rendered.

        EXAMPLES:

        We create an instance of this resource.::

            sage: import sagenb.notebook.twist
            sage: E = sagenb.notebook.twist.EmptyTrash('sage'); E
            <sagenb.notebook.twist.EmptyTrash object at ...>
        """
        self.username = username

    def http_GET(self, request):
        return http.StatusResponse(403, 'This url can only be accessed through a POST request.')
        
    def render(self, ctx):
        """
        Rendering this resource (1) empties the trash, and (2) returns a
        message.

        EXAMPLES:

        We create a notebook with a worksheet, put it in the
        trash, then empty the trash by creating and rendering this
        worksheet.::

            sage: n = sagenb.notebook.notebook.load_notebook('notebook-test.sagenb')
            sage: n.user_manager().add_user('sage','sage','sage@sagemath.org',force=True)
            sage: W = n.new_worksheet_with_title_from_text('Sage', owner='sage')
            sage: W.move_to_trash('sage')
            sage: n.worksheet_names()
            ['sage/0']
            sage: sagenb.notebook.twist.notebook = n
            sage: E = sagenb.notebook.twist.EmptyTrash('sage'); E
            <sagenb.notebook.twist.EmptyTrash object at ...>
            sage: from sagenb.notebook.twist import HTMLResponse
            sage: ctx = HTMLResponse(stream = 'foo')
            sage: ctx.headers.addRawHeader('referer', 'over there')
            sage: E.render(ctx)
            <sagenb.notebook.twist.RedirectResponse code=303, streamlen=None>

        Finally we verify that the trashed worksheet is gone::

            sage: n.worksheet_names()
            []
            sage: n.delete()
        """
        notebook.empty_trash(self.username)
        if ctx.headers.hasHeader('referer'):
            return RedirectResponse(ctx.headers.getHeader('referer'))
        else:
            return RedirectResponse('/home/' + self.username + '/?typ=trash')

class SendWorksheetToFolder(resource.PostableResource):
    def __init__(self, username):
        self.username = username

    def action(self, W):
        raise NotImplementedError

    def render(self, ctx):
        X = notebook.user(self.username)
        if user_type(self.username) == 'guest':
            return HTMLResponse(stream = message("You are not authorized to move '%s'"%W.name()))

        def send_worksheet_to_folder(filename):
            W = notebook.get_worksheet_with_filename(filename)
            self.action(W)

        if ctx.args.has_key('filename'):
            filenames = [ctx.args['filename'][0]]
        elif ctx.args.has_key('filenames'):
            sep = ctx.args['sep'][0]
            filenames = [x for x in ctx.args['filenames'][0].split(sep) if len(x.strip()) > 0]

        else:

            filenames = []

        for F in filenames:
            send_worksheet_to_folder(F)

        return HTMLResponse(stream = '')

class SendWorksheetToTrash(SendWorksheetToFolder):
    def action(self, W):
        W.move_to_trash(self.username)

class SendWorksheetToArchive(SendWorksheetToFolder):
    def action(self, W):
        W.move_to_archive(self.username)

class SendWorksheetToActive(SendWorksheetToFolder):
    def action(self, W):
        W.set_active(self.username)

# Using SendWorksheet does feel somewhat hackish.  It however is
# exactly the right thing to actually do, and minimizes code
# duplication.
class SendWorksheetToStop(SendWorksheetToFolder):
    """
    Quits each selected worksheet.
    """
    def action(self, W):
        W.quit()

############################
# Publicly Available Worksheets
############################
class PublicWorksheets(resource.Resource):
    addSlash = True

    def __init__(self, username):
        self.username = username

    def render(self, ctx):
        s = render_worksheet_list(ctx.args, pub=True, username=self.username)
        return HTMLResponse(stream = s)

    def childFactory(self, request, name):
        return Worksheet('pub/' + name, username=self.username)


############################
# Resource that gives access to worksheets
############################

class Worksheets(resource.Resource):
    def __init__(self, username):
        self.username = username

    def render(self, ctx):
        return HTMLResponse(stream = message("Please request a specific worksheet"))

    def childFactory(self, request, name):
        return WorksheetsByUser(name, username=self.username)


class WorksheetsByUserAdmin(WorksheetsByUser):
    def render(self, ctx):
        return self.render_list(ctx)

class WorksheetsAdmin(Worksheets):
    def childFactory(self, request, name):
        return WorksheetsByUserAdmin(name, username=self.username)


############################
# Adding a new worksheet
############################

class AddWorksheet(resource.Resource):
    def render(self, ctx):
        name = ctx.args['name'][0]
        W = notebook.create_new_worksheet(name)
        v = notebook.worksheet_list_html(W.name())
        return HTMLResponse(stream = encode_list([v, W.name()]))


############################

class Help(resource.Resource):
    addSlash = True
    def __init__(self, username):
        self.username = username

    def render(self, ctx):
        from tutorial import notebook_help
        return HTMLResponse(stream=template(os.path.join('html', 'docs.html'),
                                            username=self.username,
                                            notebook_help=notebook_help))


############################

############################

class History(resource.Resource):
    def __init__(self, username):
        self.username = username

    def render(self, ctx):
        t = template(os.path.join('html', 'history.html'),
                     username=self.username,
                     text = notebook.user_history_text(self.username),
                     actions=False)
        return HTMLResponse(stream=t)

class LiveHistory(resource.Resource):
    def __init__(self, username):
        self.username = username

    def render(self, ctx):
        W = notebook.create_new_worksheet_from_history('Log', self.username, 100)
        return RedirectResponse('/home/'+W.filename())


############################

class Main_css(resource.Resource):
    def render(self, ctx):
        s = css.css()
        gzip_handler(ctx)
        response = Response(stream=s)
        response.headers.addRawHeader('Content-Type', 'text/css; charset=utf-8')
        return response

class CSS(resource.Resource):
    addSlash = True

    def render(self, ctx):
        return static.File(css_path)

    def childFactory(self, request, name):
        gzip_handler(request)
        return static.File(os.path.join(css_path, name))

setattr(CSS, 'child_main.css', Main_css())

############################


############################
# Javascript resources
############################

class JSMath_js(resource.Resource):
    def render(self, ctx):
        gzip_handler(ctx)

        s = template(os.path.join('js', 'jsmath.js'),
                     jsmath_macros = jsmath_macros,
                     jsmath_image_fonts = jsmath_image_fonts)

        response = Response(stream=s)
        response.headers.addRawHeader('Content-Type',
                                      'text/javascript; charset=utf-8')
        return response

class Main_js(resource.Resource):
    def render(self, ctx):
        gzip_handler(ctx)
        s = js.javascript()

        response = Response(stream=s)
        response.headers.addRawHeader('Content-Type',
                                      'text/javascript; charset=utf-8')
        return response

class Keyboard_js_specific(resource.Resource):
    def __init__(self, browser_os):
        self.s = keyboards.get_keyboard(browser_os)

    def render(self, ctx):
        gzip_handler(ctx)

        response = Response(stream=self.s)
        response.headers.addRawHeader('Content-Type',
                                      'text/javascript; charset=utf-8')
        return response

class Keyboard_js(resource.Resource):
    def childFactory(self, request, browser_os):
        gzip_handler(request)
        return Keyboard_js_specific(browser_os)

class SageJavascript(resource.Resource):
    addSlash = True
    child_keyboard = Keyboard_js()

    def render(self, ctx):
        return static.File(sage_javascript_path)

    def childFactory(self, request, name):
        gzip_handler(request)
        path = os.path.join(sage_javascript_path, name)
        return static.File(path)

setattr(SageJavascript, 'child_main.js', Main_js())
setattr(SageJavascript, 'child_jsmath.js', JSMath_js())

class Javascript(resource.Resource):
    addSlash = True

    def render(self, ctx):
        return static.File(javascript_path)

    def childFactory(self, request, name):
        gzip_handler(request)
        if name == 'sage':
            return SageJavascript()
        path = os.path.join(javascript_path, name)
        return static.File(path)


############################
# Java resources
############################

class Java(resource.Resource):
    addSlash = True

    def render(self, ctx):
        return static.File(java_path)

    def childFactory(self, request, name):
        gzip_handler(request)
        return static.File(os.path.join(java_path, name))

############################
# Logout
############################
class Logout(resource.Resource):
    def render(self, ctx):
        # We use this class only when require_login is False.  Since
        # we haven't logged in, we just redirect to the home page.
        return http.RedirectResponse('/')

############################
# Image resource
############################

class Images(resource.Resource):
    addSlash = True

    def render(self, ctx):
        return static.File(image_path)

    def childFactory(self, request, name):
        return static.File(os.path.join(image_path, name))

#####################################
# Confirmation of registration
####################################
class RegConfirmation(resource.Resource):
    def render(self, request):
        if not notebook.conf()['email']:
            return HTMLResponse(stream=message('The confirmation system is not active.'))
        key = request.args['key'][0]
        invalid_confirm_key = """\
<h1>Invalid confirmation key</h1>
<p>You are reporting a confirmation key that has not been assigned by this
server. Please <a href="/register">register</a> with the server.</p>
"""
        key = int(key)
        global waiting
        try:
            username = waiting[key]
            user = notebook.user(username)
            user.set_email_confirmation(True)
        except KeyError:
            return HTMLResponse(stream=message(invalid_confirm_key, '/register'))
        success = """<h1>Email address confirmed for user %s</h1>""" % username
        del waiting[key]
        return HTMLResponse(stream=message(success, title='Email Confirmed'))

############################
# Registration page
############################
import re
re_valid_username = re.compile('[a-z|A-Z|0-9|_|.|@]*')
def is_valid_username(username):
    r"""
    Returns whether a candidate username is valid.  It must contain
    between 3 and 65 of these characters: letters, numbers,
    underscores, @, and/or dots ('.').

    INPUT:

    - ``username`` - a string; the candidate username

    OUTPUT:

    - a boolean

    EXAMPLES::

        sage: from sagenb.notebook.twist import is_valid_username
        sage: is_valid_username('mark10')
        True
        sage: is_valid_username('10mark')
        False
        sage: is_valid_username('me')
        False
        sage: is_valid_username('abcde' * 13)
        False
        sage: is_valid_username('David Andrews')
        False
        sage: is_valid_username('David M. Andrews')
        False
        sage: is_valid_username('sarah_andrews')
        True
        sage: is_valid_username('TA-1')
        False
        sage: is_valid_username('math125-TA')
        False
        sage: is_valid_username('dandrews@sagemath.org')
        True
    """
    import string

    if not (len(username) > 2 and len(username) < 65):
        return False
    if not username[0] in string.letters:
        return False
    m = re_valid_username.match(username)
    return m.start() == 0 and m.end() == len(username)

def is_valid_password(password, username):
    r"""
    Return True if and only if ``password`` is valid, i.e.,
    is between 4 and 32 characters long, doesn't contain space(s), and
    doesn't contain ``username``.

    EXAMPLES::

        sage: from sagenb.notebook.twist import is_valid_password
        sage: is_valid_password('uip@un7!', None)
        True
        sage: is_valid_password('markusup89', None)
        True
        sage: is_valid_password('8u7', None)
        False
        sage: is_valid_password('fUmDagaz8LmtonAowjSe0Pvu9C5Gvr6eKcC6wsAT', None)
        False
        sage: is_valid_password('rrcF !u78!', None)
        False
        sage: is_valid_password('markusup89', 'markus')
        False
    """
    import string
    if len(password) < 4 or ' ' in password:
        return False
    if username:
        if string.lower(username) in string.lower(password):
            return False
    return True

def do_passwords_match(pass1, pass2):
    """
    EXAMPLES::

        sage: from sagenb.notebook.twist import do_passwords_match
        sage: do_passwords_match('momcat', 'mothercat')
        False
        sage: do_passwords_match('mothercat', 'mothercat')
        True
    """
    return pass1 == pass2

re_valid_email = re.compile(r"""
    ^%(unquoted)s+(\.%(unquoted)s+)*    # unquoted local-part
    @                                   # at
    ([a-z0-9]([a-z0-9-]*[a-z0-9])?\.)+  # subdomains can't start or end with -
    [a-z]+$                             # top-level domain is at least 1 char
""" % {'unquoted': r"[a-z0-9!#$%&'*+\-/=?^_`{|}~]"}, re.IGNORECASE | re.VERBOSE)

def is_valid_email(email):
    """
    Validates an email address.  The implemention here is short, but
    it should cover the more common forms.  In particular, it
    allows "plus addresses," e.g.,

        first.last+label@gmail.com

    But it rejects several other classes, including those with
    comments, quoted local-parts, and/or IP address domains.  For more
    information, please see `RFC 3696`_, `RFC 5322`_, and their
    errata.

    .. _RFC 3696:   http://tools.ietf.org/html/rfc3696#section-3
    .. _RFC 5322: http://tools.ietf.org/html/rfc5322#section-3.4.1

    INPUT:

    - ``email`` - string; the address to validate

    OUTPUT:

    - a boolean; whether the address is valid, according to simplistic
      but widely used criteria

    EXAMPLES::

        sage: from sagenb.notebook.twist import is_valid_email
        sage: is_valid_email('joe@washinton.gov')
        True
        sage: is_valid_email('joe.washington.gov')  # missing @
        False
        sage: is_valid_email('foo+plus@gmail.com')
        True
        sage: is_valid_email('foo++@gmail.com')
        True
        sage: is_valid_email('foo+bar+baz@gmail.com')
        True
        sage: is_valid_email('+plus@something.org')
        True
        sage: is_valid_email('hyphens-are-okay@example.ab.cd')
        True
        sage: is_valid_email('onlytld@com')         # missing subdomain
        False
        sage: is_valid_email("we..are@the.borg")    # consecutive dots not allowed
        False
        sage: is_valid_email("abcd@[12.34.56.78]")  # legal, really
        False
        sage: is_valid_email("x@y.z")               # legal but too short
        False
        sage: is_valid_email('"i c@nt"@do.it')      # legal, really
        False
        sage: is_valid_email(65 * 'a' + '@lim.sup') # username too long
        False
        sage: is_valid_email(32 * '@..@.][.' + '!') # too long, ...
        False
    """
    if 7 < len(email) < 257:
        if re_valid_email.match(email) is None:
            return False
        # TODO: If/when we permit *quoted* local-parts, account for
        # legal additional @'s, e.g., "foo@bar"@bar.foo
        if len(email.split('@')[0]) > 64:
            return False
        return True
    return False

def user_type(username):
    # one of admin, guest, user
    try:
        U = notebook.user(username)
    except KeyError:
        return 'guest'
    return U.account_type()

def extract_title(html_page):
    h = html_page.lower()
    i = h.find('<title>')
    if i == -1:
        return "Untitled"
    j = h.find('</title>')
    return html_page[i + len('<title>') : j]
